# -*- coding: utf-8 -*-
"""
Created on Tue Nov 20 20:39:39 2018

@author: 陈忠涛
"""
# =======实现步骤=======
# 使用webdriver登陆自己的qq空间并获取cookies：很重要，cookie里面的参数在后面获取信息的时候（请求）起到重要作用
# 获取g_tk值。参考：https://blog.csdn.net/Vision_Tung/article/details/78888695
# 获取qzondetoken值。返回界面中有相应js脚本：window.g_qzonetoken = (function(){ try{return "*********";} catch(e) {var xhr = new XMLHttpRequest();xhr.withCredentials = true;xhr.open('post', '//h5.qzone.qq.com/log/post/error/qzonetoken', true);xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');xhr.send(e);}})();
# 获取到所有qq好友。有一个特定的请求地址【必需参数：g_tk、qzondetoken】
# 根据好友qq进行爬取【必需参数：g_tk、qzondetoken、pos】
# 保存数据进本地mysql数据库
# 等待所有数据爬取完成之后，进行数据库读取并执行执行 “词云” 绘制
# 程序结束

# 数据库创建【我使用的是“HeidiSQL”这款可视化软件】
# CREATE TABLE `zone_data` (
#     `id` INT(7) NOT NULL AUTO_INCREMENT,
#     `qq` VARCHAR(15) NOT NULL,
#     `nickname` VARCHAR(20) NOT NULL,
#     `content` VARCHAR(500) NULL DEFAULT NULL,
#     `create_time` VARCHAR(20) NOT NULL,
#     PRIMARY KEY (`id`)
# )

from selenium import webdriver # 导入selenium包中的webdriver模块
import pymysql # 引入pymysql模块
import wx  # 引入 wxpython GUI模块
import requests
import time
import re 
import math
import pandas as pd
import numpy as np
from PIL import Image
import matplotlib.pyplot as plt
import jieba
from wordcloud import WordCloud, STOPWORDS


class Q_zone_operation(wx.App):
    def OnInit(self):
        self.frame_login = wx.Frame(None, title = 'qq空间登陆', size = (300, 400))
        # 存放标题
        panel = wx.Panel(self.frame_login, size = (300, 400))
        panel.SetBackgroundColour('#ffffff')
        sizer = wx.BoxSizer(wx.VERTICAL)
        login_title = wx.StaticText(panel,-1, label = '登陆QQ空间', style = wx.ALIGN_CENTER)
        title_font = wx.Font(20, wx.ROMAN, wx.NORMAL, wx.NORMAL)
        login_title.SetFont(title_font)
        sizer.Add(login_title,proportion=0, flag=wx.ALL|wx.EXPAND, border=15)

        # 用于存放用户名+密码框+登录按钮
        login_fgs = wx.FlexGridSizer(3,2,9,25)
        login_username = wx.StaticText(panel, label = '用户名：')
        login_password = wx.StaticText(panel, label = '密码：')
        self.login_t_u = wx.TextCtrl(panel)
        self.login_t_p = wx.TextCtrl(panel, style=wx.TE_PASSWORD)
        login_fgs.AddMany([(login_username),(self.login_t_u,1,wx.EXPAND),(login_password),(self.login_t_p,1,wx.EXPAND)])
        
        login_fgs.AddGrowableRow(2,1)
        login_fgs.AddGrowableCol(1,1)
        sizer.Add(login_fgs, proportion=0, flag = wx.ALL|wx.EXPAND, border = 15)

        login_button = wx.Button(panel, label = '登录') # 登陆按钮
        login_button.Bind(wx.EVT_BUTTON, self.into_qq_zone) # 点击按钮进行登陆
        sizer.Add(login_button,proportion = 0, flag = wx.ALL|wx.EXPAND, border=15)

        panel.SetSizer(sizer)
        self.frame_login.Show()
        return True
    
    def into_qq_zone(self, event):
        '模拟人的操作,执行登陆操作'
        my_u = self.login_t_u.GetValue() # 用户名
        my_p = self.login_t_p.GetValue() # 密码
        browser = webdriver.Chrome() # 使用chrome浏览器作为自动化测试（需要到）谷歌浏览器版本：70.0.3538.110（正式版本） （64 位）
        browser.get('https://qzone.qq.com/')
        browser.switch_to_frame('login_frame') # 观察到登陆界面，发现登录框是一个frame框架，所以要切换进frame。否则就不能选择元素节点。

        # 寻找到页面html元素,在选择元素的时候，尽量使用（css选择器）id选择器，因为id属性是唯一的，不可重复的。
        browser.find_element_by_id('switcher_plogin').click() # 使用账号、密码登陆时，需要通过其页面的一个点击事件。
        browser.find_element_by_id('u').send_keys(my_u) # qq账号
        browser.find_element_by_id('p').send_keys(my_p) # qq密码
        browser.find_element_by_id('login_button').click() # 登陆
        browser.switch_to.default_content() # 设置成默认定位

        time.sleep(5)   # 超级坑，一直获取不到登陆后的cookie, 原来需要延迟一点时间 
        
        n_session = self.get_session(browser) # 返回带cookies的requests
        my_cookie = browser.get_cookies() # 获取登陆后的cookies
        new_cookies = {} # 用一个字典来维护cookies
        for item in my_cookie:
            new_cookies[item['name']] = item['value']

        # 获取g_tk参数。这是一个空间加密算法。根据实际操作发现：获取好友列表，在cookies中需要用到g_tk参数【必要参数之一】
        g_tk = self.get_g_tk(new_cookies) 

        # 获取qzondetoken参数，这个参数可以在网页代码中的js脚本里获得【必要参数之一】
        # 来自js脚本：window.g_qzonetoken = (function(){ try{return "*********";} catch(e) {var xhr = new XMLHttpRequest();xhr.withCredentials = true;xhr.open('post', '//h5.qzone.qq.com/log/post/error/qzonetoken', true);xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');xhr.send(e);}})();
        html = browser.page_source
        s = re.compile(r'window\.g_qzonetoken = \(function\(\)\{ try\{return (.*?);\} catch\(e\)')
        qzondetoken = re.findall(s, html)[0]

        # 获取所有qq好友
        all_q_df = self.get_friend_list(my_u, n_session, g_tk, qzondetoken)
        # 连接数据库【一旦出了异常，再次“登陆空间”时，会出现：Already closed 异常，所以放在本函数内。】
        self.conn = pymysql.connect(host='localhost',port= 3306,user = 'root',passwd='chenzt',db='qq_zone') #db：库名
        # 采取循环的方式进行每位好友空间的爬取
        for qq in all_q_df.index:
            # 采用异常处理机制
            try:
                # 爬取qq好友说说
                self.get_qq_article(qq, all_q_df.loc[qq]['nickname'], n_session, g_tk, qzondetoken)
            except Exception as e:
                print('爬取失败~下一位~~\n', e)
            else:
                print(str(qq) + ' :空间数据爬取完毕\n')
        else:
            # 断开连接，意味着数据已经全部爬取完毕，登录框也应该关闭（或许可以在进入空间的时候关闭吧），等待几秒打开显示词云的界面
            self.conn.close()
            self.frame_login.Show(False)
            time.sleep(3)
            browser.close() # 关闭浏览器
            self.show_wordCloud()
            exit()

    def get_session(self, browser):
    	'获取cookies,返回带cookies 的requests'
    	my_session = requests.session() # 获取自动保存的cookies
    	my_cookie = browser.get_cookies() # 获取登陆后的cookies
    	new_cookies = {} # 用一个字典来维护cookies
    	for item in my_cookie:
    		new_cookies[item['name']] = item['value']
    	headers={   'host': 'user.qzone.qq.com',
				'accept-encoding':'gzip, deflate, br',
				'accept-language':'zh-CN,zh;q=0.9',
				'accept':'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',
				'user-agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36',
				'connection': 'keep-alive'
				}
    	n = requests.utils.cookiejar_from_dict(new_cookies,cookiejar=None,overwrite=True) # https://blog.csdn.net/qq_37786775/article/details/80451403
    	my_session.headers = headers
    	my_session.cookies.update(n)
    	return my_session

    def get_friend_list(self,my_qq , n_session, my_g_tk, my_qzondetoken):
        '获取所有QQ好友'
        all_friend_url = 'https://user.qzone.qq.com/proxy/domain/r.qzone.qq.com/cgi-bin/tfriend/friend_ship_manager.cgi?uin=' + str(my_qq) + '&do=1&rd=0.2255256962073533&fupdate=1&clean=1&g_tk=' + str(my_g_tk) + '&qzonetoken=' + str(my_qzondetoken) + '&g_tk=' + str(my_g_tk)
        all_friend_text = n_session.get(all_friend_url).text

        # qq号
        q_compile = re.compile('"uin":(.*?),')
        all_friend_qq = re.findall(q_compile, all_friend_text)

        # qq昵称
        q_compile = re.compile('"name":(.*?),')
        all_friend_nickname = re.findall(q_compile, all_friend_text)
        
        # 维护成一个DataFrame
        q_df = pd.DataFrame()
        q_df['nickname'] = all_friend_nickname
        q_df.index = all_friend_qq
        return q_df
    
    def get_qq_article(self, f_qq, f_nickname, n_session, my_g_tk, my_qzondetoken):
        '爬取好友说说'
        # 通过空间请求地址可以得出以下url    【测试，先获取自己的说说】
        all_art_url = 'https://user.qzone.qq.com/proxy/domain/taotao.qq.com/cgi-bin/emotion_cgi_msglist_v6?uin=' + str(f_qq) + '&ftype=0&sort=0&pos=0&num=20&replynum=100&g_tk=' + str(my_g_tk) + '&callback=_preloadCallback&code_version=1&format=jsonp&need_private_comment=1&qzonetoken=' + str(my_qzondetoken) + '&g_tk=' + str(my_g_tk)
        all_article_text = n_session.get(all_art_url).text

        # 说说内容
        content_compile = re.compile('"certified".*?"conlist":(.*?),')
        # 说说创建时间
        create_time_text = re.compile('"certified".*?"createTime":(.*?),')

        content_list_text = re.findall(content_compile, all_article_text)
        # 判断该好友的说说空间是否限制访问，或者根本就没有发布过qq说说
        if len(content_list_text) == 0:
            print('啊哦，好友： ' + f_nickname + ' 设置了空间访问权限或者没发过还说说~~')
            return False # 直接返回

        # 因为这里使用的“页数翻页”，所以要计算出总的页数（每一页都进行爬取，直到最后一页）
        total_compile = re.compile('"total":(.*?),')
        total = re.findall(total_compile, all_article_text)[0]
        # 由于每页说说是num=20条，所以最终可以计算出总页数（2种情况：刚好是20的倍数；不是20的倍数。额~~当我没说~_~）
        page = 0
        pos = 0
        if int(total) % 20 == 0:
            page = int(total) // 20
        else:
            page = math.ceil(int(total) / 20)

        # 爬取每位好友的所有说说
        for c in range(0, int(page)):
            pos = c * 20
            paging_url = 'https://user.qzone.qq.com/proxy/domain/taotao.qq.com/cgi-bin/emotion_cgi_msglist_v6?uin=' + str(f_qq) + '&ftype=0&sort=0&pos=' + str(pos) + '&num=20&replynum=100&g_tk=' + str(my_g_tk) + '&callback=_preloadCallback&code_version=1&format=jsonp&need_private_comment=1&qzonetoken=' + str(my_qzondetoken) + '&g_tk=' + str(my_g_tk)
            paging_all_text = n_session .get(paging_url).text

            # 每页说说内容
            content_list_text_p = re.findall(content_compile, paging_all_text)
            # 说说创建时间
            create_time_text_p = re.findall(create_time_text, paging_all_text)
            # 使用 zip函数把对应的 说说内容 和 创建时间 做对应处理
            # 考虑到数据过于庞大，我就不用文件的形式保存了， 直接用mysql数据库来存
            for (content, time) in zip(content_list_text_p, create_time_text_p):
                # 替换源数据中的“con”属性名
                content = content.replace('[{"con":', '')
                try:
                    self.database_operation([f_qq, f_nickname, content, time])
                except Exception as e:
                    print(e)
                else:
                    print('......写入完毕')

    def show_wordCloud(self):
        '获取数据库所有的数据，进行词云图绘制'
        try:
            # 连接数据库
            once_conn = pymysql.connect(host='localhost',port= 3306,user = 'root',passwd='chenzt',db='qq_zone')
            # 创建游标
            cursor = once_conn.cursor()
        except Exception as e:
            print(e,'数据库连接或者创建游标失败：code 219 line')
        else:
            sql = "select `content` from zone_data"
            cursor.execute(sql)
            once_conn.commit()
            all_data = cursor.fetchall() # 返回所有数据
            new_str = ''
            if len(all_data) != 0:
                for item in all_data:
                    s_item = str(item[0])
                    if s_item == 'null':
                        continue
                        s_item = s_item[1:len(s_item)-1] # 去除首尾双引号
                    new_str += s_item
                # 开始绘图
                seg_list = jieba.cut_for_search(new_str)
                seg_str = ','.join(seg_list)
                alice_mask = np.array(Image.open('./huli.png')) # 打入背景图片
                stopwords = set(STOPWORDS) 

                wc = WordCloud(background_color="white", max_words=2000, mask=alice_mask,stopwords=stopwords, contour_width=3, contour_color='steelblue',font_path = 'C:/Users/Windows/fonts/simkai.ttf')
                wc.generate(seg_str)
                # 显示
                plt.imshow(wc, interpolation='bilinear')
                plt.axis("off")
                plt.figure()
                plt.imshow(alice_mask, cmap=plt.cm.gray, interpolation='bilinear')
                plt.axis("off")
                plt.show()
            else:
                print('数据库为空！')
                return
        finally:
            # 关闭数据库连接
            once_conn.close()

    def get_g_tk(self, cookie):
        '空间加密算法,【百度一下】：https://blog.csdn.net/Vision_Tung/article/details/78888695'
        hashes = 5381
        for letter in cookie['p_skey']:
            hashes += (hashes << 5) + ord(letter)
        return hashes & 0x7fffffff

    def database_operation(self, data_list):
        '数据库操作: 插入一条数据'
        qq = data_list[0] # qq
        nickname = data_list[1] # 好友名字
        content = data_list[2] # 说说内容
        time = data_list[3] # 创建时间
        print(qq,content,time)
        # 插入一条数据
        sql = "insert into zone_data (`qq`, `nickname`, `content`, `create_time`) VALUES('" + qq + "','" + nickname + "','" + content + "','" + time + "')"
        self.conn.query(sql)
        self.conn.commit()   # 就因为少了句“提交”，导致我爬了8万多条数据没有添加进数据库~_~
if __name__ == '__main__':
    app = Q_zone_operation()
    app.MainLoop()